package org.juiceshop.gm;

import groovy.lang.Closure;

import java.util.ArrayList;
import java.util.List;

import javax.sound.midi.Sequence;

import org.juiceshop.gm.midi.Instrument;
import org.juiceshop.gm.midi.SequenceBuilder;
import org.juiceshop.gm.midi.TrackBuilder;

public class Score {

	static final int DEFAULT_TEMPO = 120;
	static final Instrument DEFAULT_INSTRUMENT = Instrument.PIANO;
	static final int MAX_CHANNEL = 16;

	
	public static Score create (final Closure closure) {
		Score score = new Score();
		Closure closureCopy = (Closure)closure.clone();
		closureCopy.setDelegate(score);
		closureCopy.setResolveStrategy(Closure.DELEGATE_FIRST);
		MusicObjectCategory.use(closureCopy);
		score.close();
		return score;
	}
	
	public static Score create (final MusicObject obj) {
		Score score = new Score();
		score.append(obj);
		return score;
	}
	
	private SequenceBuilder _sb;
	private List<Voice> _voices;
	
	private int _channelCount = 0;
	private int _tempo;
	private boolean _verbose = false;
	
	
	public Score() {
		_sb = new SequenceBuilder();
		_voices=new ArrayList<Voice>();
		setTempo(DEFAULT_TEMPO);
	}
	
	public Score(final int voices) {
		this();
		for (int i=0; i<voices; i++) {
			voice();
		}
	}
	
	public Voice voice() {
		if (_channelCount>=MAX_CHANNEL) {
			throw new IllegalStateException("Too many channels.");
		}
        TrackBuilder trackBuilder = _sb.getTrackBuilder(_channelCount++);
		// leave out channel 9 for percussion instruments
		if (_channelCount==9) _channelCount++;
		Voice voice = new Voice(_voices.size(),DEFAULT_INSTRUMENT,trackBuilder);
        _voices.add(voice);
        return voice;
	}
	
	public Voice voice (final Instrument instr) {
		Voice voice = voice();
		voice.setInstrument(instr);
		return voice;
	}
	
	public Voice getVoice() {
        return getVoice(0);
	}

	public Voice getVoice(final int index) {
        if (index==0 && _voices.size()==0) voice();
		if (index<0 || index>=_voices.size()) {
			throw new IndexOutOfBoundsException("Undefined voice number: "+index);
		}
		return _voices.get(0);
	}
	
	
	public void setTempo(final int tempo) {
		_tempo = tempo;
		_sb.tempo(tempo);
	}
	
	public int getTempo() {
		return _tempo;
	}
	
	public void setInstrument(final Instrument instr)  {
		getVoice().setInstrument(instr);
	}
	
	public Instrument getInstrument() {
		return getVoice().getInstrument();
	}
	
	public void setOctave(final int octave) {
		getVoice().setOctave(octave);
	}
	
	public int getOctave() {
		return getVoice().getOctave();
	}
	
	public Score append (final MusicObject obj) {
        getVoice().append(obj);
        return this;
	}
	
	public Score leftShift (MusicObject obj) {
		return append(obj);
	}
	
	public void sync() {
		long maxTicks = _sb.getMaxTicks();
		for (Voice v : _voices) {
			v.fillUp(maxTicks);
		}
		_sb.sync();
	}
	
	public void close() {
		sync();
		for (Voice v : _voices) {
			v.close();
		}
		_sb.close();
	}
	
	public Sequence getSequence() {
		return _sb.getSequence();
	}

	public void setVerbose(boolean verbose) {
		_verbose = verbose;
	}
	
	public boolean isVerbose() {
		return _verbose;
	}


	public class Voice {
		private int _number;
		private String _name;
		private TrackBuilder _tb;
		private Instrument _instrument;
		private int _octave;
		private MusicObject _filler;
	
		
		private Voice (int number, Instrument instrument, TrackBuilder trackBuilder) {
			_number = number;
			_instrument = instrument;
			_tb = trackBuilder;
		}
		
		public Voice append (MusicObject obj) {
			if (obj instanceof Note) {
				appendNote((Note)obj);
			} else if (obj instanceof Phrase) {
				appendPhrase((Phrase)obj);
			} else if (obj instanceof Chord) {
				appendChord((Chord)obj);
			} else {
				throw new IllegalStateException("Unknown MusicObject type: "+obj.getClass());
			}
			return this;
		}
		
	    public Voice appendPhrase (Phrase phrase) {
	        for (MusicObject obj : phrase) {
	            append(obj);
	        }
	        return this;
	    }
	    
	    public Voice appendNote (Note note, double offset) {
	    	if (isVerbose()) {
	    		System.out.println(note);
	    	}
	        long lengthTicks = SequenceBuilder.calculateTicks(note.getValue());
            long oldTimer = _tb.getTimer();
	        if (!note.isRest()) {
	        	int pitch = note.getPitch() + _octave*12;
	            long offsetTicks = SequenceBuilder.calculateTicks(offset);
	            long durationTicks = SequenceBuilder.calculateTicks(note.getDuration());
	            _tb.setTimer(oldTimer+offsetTicks);
	            _tb.noteOn(pitch, note.getDynamic());
	            _tb.setTimer(oldTimer+offsetTicks+durationTicks);
	            _tb.noteOff(pitch, note.getDynamic());
	        }
	        _tb.setTimer(oldTimer+lengthTicks);
	        return this;
	    }
	    
	    public Voice appendNote (Note note) {
	    	return appendNote(note,0d);
	    }
	    
	    public Voice appendChord (Chord chord) {
	        long saveTimer = _tb.getTimer();
	        double offset = 0d;
	        for (MusicObject obj : chord) {
	        	if (obj instanceof Note) {
		            appendNote((Note)obj,offset);
	        	} else {
	        		append(obj);
	        	}
	            _tb.setTimer(saveTimer);
	            offset += chord.getArpeggio();
	        }
	        _tb.advance(SequenceBuilder.calculateTicks(chord.getValue()));
	        return this;
	    }
	    
	    public Voice leftShift (MusicObject obj) {
	    	return append(obj);
	    }
	    
	    public int getNumber() {
			return _number;
		}
	    
	    public String getName() {
	    	if (_name==null) {
	    		return _instrument.toString()+"/"+_number; 
	    	}
			return _name;
		}
	    
	    public void setName(String name) {
			_name = name;
		}
	    
	    public void setInstrument (Instrument instrument) {
	        _instrument = instrument;
	        _tb.programChange(instrument.ordinal());
	    }
	    
	    public Instrument getInstrument () {
	    	return _instrument;
	    }
	    
	    public void setOctave(int octave) {
			_octave = octave;
		}
	    
	    public int getOctave() {
			return _octave;
		}
	    
	    public MusicObject getFiller() {
			return _filler;
		}

		public void setFiller(MusicObject filler) {
			_filler = filler;
		}
		
		public void fillUp(long toTick) {
			System.out.println(getName()+" - fill up to "+toTick);
			while (_filler!=null && _tb.getTimer()<toTick) {
				append(_filler);
			}
		}

		public void close() {
	        // Add a whole note bonus rest
	    	_tb.advance(SequenceBuilder.WHOLE);
	    }

	}
}
